/**
 * 
 */
package jnzb.control;

import java.io.IOException;

import jnzb.control.events.InfoEventProcessor;
import jnzb.control.loader.NZBStorage;
import jnzb.control.queue.QueueManager;
import jnzb.control.util.FileUtils;
import jnzb.misc.StorageUnit;
import jnzb.model.nzb.File;
import jnzb.model.nzb.NZB;
import jnzb.model.queue.FinishedTableModel;
import jnzb.model.queue.WaitingTableModel;

/**
 * Class responsible for the full process behind NZB processing
 * 
 * @author Rid
 * 
 */
public class NZBProcessor implements Runnable {

	private boolean processQueue = true;

	private Controller controller;

	private Downloader downloader;

	private Decoder decoder;

	private boolean shutdown = false;

	private FinishedTableModel finishedModel = new FinishedTableModel();

	private InfoEventProcessor infoProcessor;

	private Configuration config;

	private QueueManager queueManager;

	private DownloadStartedListener downloadStartedListener = null;

	private NZB currentNZB;

	private boolean actioningQueueChange;

	public NZBProcessor(Controller controller, InfoEventProcessor infoProcessor) {
		this.controller = controller;
		this.infoProcessor = infoProcessor;

		this.config = Configuration.getInstance();

		this.downloader = new Downloader(controller, this, infoProcessor);
		this.decoder = new Decoder(controller, this, infoProcessor);
		this.queueManager = new QueueManager();

		// Register the waiting table model to listen for download speed updates
		this.downloader.registerListener(this.queueManager.getTableModel());

		// Kick off the queue monitor in a separate thread
		new Thread(this).start();
	}

	/**
	 * Add a valid nzb file to the queue
	 * 
	 * @param nzb
	 */
	public synchronized void addNZB(NZB nzb) {
		queueManager.addNZB(nzb);

		// Wake up thread to check if we can download
		notifyAll();
	}

	public synchronized void stop() {
		infoProcessor.debug("nzbprocess.stop");
		this.processQueue = false;
		this.downloader.exit();
		notifyAll();
	}

	/**
	 * If the nzb queue changes, tell the downloader to stop what it's doing
	 */
	public synchronized void queueChanged() {
		this.downloader.stopDownloading();
		this.actioningQueueChange = true;
	}

	public NZB getCurrentNZB() {
		return this.downloader.getCurrentNZB();
	}

	/**
	 * Main queue monitor. If it finds something in the queue it'll start
	 * processing in the same thread. When finished processing, goes back to
	 * checking the queue.
	 * 
	 * @see java.lang.Runnable#run()
	 */
	@Override
	public synchronized void run() {
		while (processQueue) {
			if (!this.downloader.isDownloading()
					&& !this.queueManager.isQueueEmpty()
					&& !queueManager.getNextNZB().equals(this.currentNZB)) {
				this.currentNZB = queueManager.getNextNZB();
				queueNZB(this.currentNZB);
			} else {
				infoProcessor.debug("downloading = "
						+ this.downloader.isDownloading());
				infoProcessor.debug("is queue empty = "
						+ this.queueManager.isQueueEmpty());
				if (queueManager.getNextNZB() != null) {
					infoProcessor
							.debug("is next nzb the same as the current one? = "
									+ queueManager.getNextNZB().equals(
											currentNZB));
				} else {
					infoProcessor.debug("There is no next nzb");
				}
				try {
					infoProcessor.debug("NZBProcess going to sleep");
					wait();
					infoProcessor.debug("NZBProcess waking");
				} catch (InterruptedException e) {
					// log an error here
					e.printStackTrace();
				}
			}
		}
		shutdown = true;
		infoProcessor.debug("nzbprocess exiting");
	}

	private void queueNZB(NZB nzb) {
		downloader.startDownload(nzb);
		if (this.downloadStartedListener != null) {
			infoProcessor
					.debug("QNZB:Informing registered download listener of start");
			this.downloadStartedListener.downloadStarted();
			this.downloadStartedListener = null;
		} else {
			infoProcessor.debug("QNZB:No download listener registered");
		}
	}

	public void startNewNZB(NZB nzb) throws IOException {
		FileUtils.createWorkFolderStructure(nzb);

		// First, update the panel with the data we know about at the moment!
		long size = nzb.getSize();
		String formattedSize = StorageUnit.of(size).format(size);

		String contains = nzb.getNumberOfFiles() + " files.  Approximately "
				+ formattedSize;

		controller.getUiDelegate().setNZBDetails(nzb.getFilename(), contains,
				nzb.getSize());
		;

	}

	/**
	 * Callback method from the downloader that indicates a file is finished. We
	 * use it to add it to the decoder queue
	 * 
	 * @param downloadedFile
	 */
	public void readyToDecode(File downloadedFile) {
		if (downloadedFile.allSegmentsDownloaded()
				&& !downloadedFile.isDecoded()) {
			// System.out.println("All parts downloaded - Can decode this file");
			this.decoder.addFile(downloadedFile);
		} else {
			infoProcessor
					.error("Error decoding "
							+ downloadedFile.getSubject()
							+ " - parts missing. Try adding NZB again when finished to complete download.");
		}
	}

	/**
	 * Called when the nzb download is complete - i.e every segment has run
	 * through the downloader
	 * 
	 * @param nzb
	 */
	public synchronized void nzbDownloadComplete(NZB nzb) {
		// Trigger gui update
		infoProcessor.info("NZB Finished");
		controller.getUiDelegate().nzbFinished(nzb);

		// Need to remove also if there was a severe error downloading
		// (newsConnectionException)
		if (nzb.getAllAttempted()) {
			// Remove from the queue
			queueManager.removeNZB(nzb);

			this.finishedModel.nzbFinished(nzb);
			infoProcessor.info("NZB removed from queue");
		} else {
			infoProcessor.info("NZB not removed from queue");
		}
		this.currentNZB = null;
		// Wake up this thread to continue processing next nzb to download
		notifyAll();

		// Notify the listener if there is one
	}

	/**
	 * Indicate that a file has completed decoding
	 * 
	 * @param file
	 */
	public void decodeComplete(File file) {
		// Check if launching par2
		if (config.autoLaunchPar()) {
			if (file.getOutputFilename() != null) {
				String f = file.getOutputFilename().toLowerCase().trim();
				infoProcessor.debug("Par2 filename=" + f);
				boolean vol = f.indexOf("vol") > -1;
				boolean par2 = f.indexOf(".par2") > -1;
				// Launch files that contain .par2 but not vol
				if (!vol && par2) {
					try {
						infoProcessor.debug("Trying to launch"
								+ file.getAbsolutePath()
								+ file.getOutputFilename());
						Runtime.getRuntime().exec(
								file.getAbsolutePath()
										+ file.getOutputFilename());
					} catch (IOException e) {
						infoProcessor.error("Error launching PAR2 file");
						infoProcessor.debug("Par2 launch error - "
								+ e.getMessage());
					}
				}
			}
		}

		// Update progress
		checkProgress(file.getRootNZB());
	}

	/**
	 * This checks if all the files in the NZB have been decoded, in which case
	 * it's finished and we can tidy up. Called by the decoder when it finishes
	 * decoding a file
	 */
	public void checkProgress(NZB nzb) {
		if (nzb.allDecoded()) {
			NZBStorage.deleteProgressFile(nzb.getFilename());
			NZBStorage.deleteSegmentsDirectory(nzb.getFilename());

		} else {
			try {
				NZBStorage.saveProgress(nzb);
			} catch (IOException e) {
				infoProcessor.error("Could not save progress file");
			}
		}
	}

	/**
	 * Returns the reference to the downloader
	 * 
	 * @return
	 */
	public Downloader getDownloader() {
		return downloader;
	}

	/**
	 * Returns the table model storing the nzb queue
	 * 
	 * @return
	 */
	public WaitingTableModel getQueueModel() {
		return queueManager.getTableModel();
	}

	public QueueManager getQueueManager() {
		return queueManager;
	}

	/**
	 * Checks if this thread is shutdown (i.e has exited) and the same for the
	 * downloader
	 * 
	 * @return
	 */
	public boolean isShutdown() {
		return this.shutdown && this.downloader.isShutdown();
	}

	/**
	 * Returns the table model storing the finished NZB list
	 * 
	 * @return
	 */
	public FinishedTableModel getFinishedModel() {
		return this.finishedModel;
	}

	/**
	 * Registers an object to call when the current NZB finished. Note that only
	 * one object can register at a time (and should do!) and that once notified
	 * this is set to null so they have to re-register
	 * 
	 * @param queueChangeProcessor
	 */
	public void registerDownloadFinishedListener(
			DownloadStartedListener queueChangeListener) {
		infoProcessor.debug("download finished listener registered");
		this.downloadStartedListener = queueChangeListener;
	}

	/**
	 * Callback method from the downloader to indicate it's finished downloading
	 * an nzb. This may be because of an error or not
	 * 
	 * @param currentNZB
	 *            - the nzb downloaded
	 * @param stoppedByError
	 *            - indicates if an error stopped the downloader
	 * @param interrupted
	 *            - indicates if the downloader was interrupted
	 */
	public void downloadFinished(NZB currentNZB, boolean stoppedByError,
			boolean interrupted) {
		// If it finished 'naturally'
		infoProcessor.debug("downloadFinished");
		if (!stoppedByError) {
			infoProcessor.debug("not stopped by error");
			if (!interrupted) {
				infoProcessor.debug("not interrupted");
				// 
				nzbDownloadComplete(currentNZB);
			} else {
				infoProcessor.debug("interrupted");
				// We were interrupted.
				// Could be pause or a queue change
				// If a pause, we need to do nothing
				// If its a queue change, we need to call notify!
				infoProcessor.debug("Downloader interrupted");
				if (this.actioningQueueChange) {
					infoProcessor
							.debug("Actioning a queue change - waking up thread");
					synchronized (this) {
						notifyAll();
					}
				}
			}
		} else {
			infoProcessor.debug("stopped by error");
			// Stopped by an error
			int retryTime = config.getAutoRetryValue();
			if (retryTime > 0) {
				infoProcessor.error("Error downloading.  Auto restart in "
						+ retryTime + " seconds or press restart to try now");
				// Start auto retry timer
				controller.startRetryTimer();
			} else {
				infoProcessor
						.error("Error downloading.  Press restart to try again (no retry value defined)");
			}
			// Set the UI up as if paused
			controller.getUiDelegate().setPauseDisplay(true);
		}
		// Nothing to do, ensure that display is clear
		controller.getUiDelegate().resetNZBDisplay();

		infoProcessor.debug("setting actioning queue change to false");
		actioningQueueChange = false;

	}

	/**
	 * Toggle the pause state, which is not really paused, it stopped or started
	 */
	public void togglePaused() {
		// Are we downloading?
		if (downloader.isDownloading()) {
			downloader.stopDownloading();
			controller.getUiDelegate().setPauseDisplay(true);
		} else {
			// Restart the NZB at the head of the queue
			// Just wake up the processor, it will start the current NZB
			currentNZB = null;
			synchronized (this) {
				notifyAll();
			}
			controller.getUiDelegate().setPauseDisplay(false);
		}
	}
}
